using System;
using System.Text;
using System.Reflection;
using System.Data;
using Utility.Logging;

namespace Tort.Database.Language
{
    internal abstract class DBSyntax 
    {
        /// <summary>
        /// Constructor to auto generate sql syntax dependant on engine
        /// </summary>
        /// <param name="obj">The object to persist</param>
        /// <param name="command">the command object to return</param>
        internal DBSyntax(object obj, IDbCommand command, QueryType queryType, Providers.DbDetails details)
        {
            Obj = obj;
            Command = command;
            QueryType = queryType;
            Details = details;
            BuildORMapping();
        }

        protected StringBuilder beginSql = new StringBuilder();
        protected Translation.ORMapping mapping;
        protected StringBuilder endSql = new StringBuilder();

        private Language.QueryType queryType;
        private Language.QueryType QueryType
        {
            get { return queryType; }
            set { queryType = value; }
        }

        private object obj;
        protected object Obj
        {
            get { return obj; }
            set { obj = value; }
        }

        private IDbCommand command;
        internal IDbCommand Command
        {
            get { return command; }
            set { command = value; }
        }

        private string parameterName;
        internal string ParameterName
        {
            get { return parameterName; }
            set { parameterName = value; }
        }

        private object primeKey;
        protected object PrimeKey
        {
            get { return primeKey; }
            set { primeKey = value; }
        }

        private Providers.DbDetails Details { get; set; }

        /// <summary>
        /// Loads the object passed to the contructer and 
        /// dissembles it into sql columns, values and types
        /// </summary>
        private void BuildORMapping()
        {
            mapping = new Translation.ORMapping();
            mapping.TableName = ((Attributes.DBObjectAttribute)Obj.GetType().GetCustomAttributes(typeof(Attributes.DBObjectAttribute), false)[0]).TableName;
            if (mapping.TableName == null) mapping.TableName = Obj.GetType().Name; //entirely reasonable to assume object name = table name
            int i = 0;

            foreach (PropertyInfo propertyInfo in Obj.GetType().GetProperties())
            {
                if (propertyInfo.GetCustomAttributes(typeof(Attributes.DBFieldAttribute), false).Length > 0) // its persistable
                {
                    if (propertyInfo.GetCustomAttributes(typeof(Attributes.PrimeKeyAttribute), false).Length == 1)
                    {
                        Attributes.PrimeKeyAttribute pkAttr = (Attributes.PrimeKeyAttribute)propertyInfo.GetCustomAttributes(typeof(Attributes.PrimeKeyAttribute), false)[0];
                        if (pkAttr.Generator == Attributes.PrimeKeyGenerator.Application)
                        {
                            // we need to persist but we know the PK value already
                            ParsePropertyInfo(pkAttr, propertyInfo, true, false);
                        }
                        else
                        {
                            // We need to persist and return the prime key value
                            ParsePropertyInfo(pkAttr, propertyInfo, true, true);
                        }
                    }
                    else if (propertyInfo.GetCustomAttributes(typeof(Attributes.PrimeKeyAttribute), false).Length > 1)
                    {
                        TortException.PersistenceException pe = new TortException.PersistenceException("You cannot have more than one prime key per object!!!");
                        // caught by application layer since they shouldnt be being silly.
                        throw pe;
                    }
                    else
                    {

                        Attributes.DBFieldAttribute dbFieldAttr = (Attributes.DBFieldAttribute)propertyInfo.GetCustomAttributes(typeof(Attributes.DBFieldAttribute), false)[0];

                        // could be a complex object..
                        if (dbFieldAttr.GetType() == typeof(Attributes.LoadRequirementAttribute))
                        {
                            // extract the link column only....
                            // In the event of a select statement, its always going to be null so....
                            object o = propertyInfo.GetValue(Obj, null);
                            if (o == null)
                            {
                                ParseComplexObject(dbFieldAttr, Activator.CreateInstance(propertyInfo.PropertyType));
                            }
                            else
                            {
                                ParseComplexObject(dbFieldAttr, propertyInfo.GetValue(Obj, null));
                            }
                        }
                        else
                        {
                            ParsePropertyInfo(dbFieldAttr, propertyInfo);
                        }
                    }
                }
                i++;
            }
        }

        /// <summary>
        /// Parses the propertyInfo passed in and validates data
        /// If the column being passed is null value, the type is derived from the property itself
        /// rather than relying on the property in entirety
        /// This is so none null objects get the SQL parameters built correctly with the right length
        /// rather than varchar(8000) eg.
        /// </summary>
        /// <param name="dbAttributes"></param>
        /// <param name="propertyInfo"></param>
        private void ParsePropertyInfo(Attributes.DBFieldAttribute dbAttributes, PropertyInfo propertyInfo, bool isPrimeKey, bool isPrimeKeyRequired)
        {
            BuildColumMapping(dbAttributes.ColumnName == null ?
                propertyInfo.Name : dbAttributes.ColumnName, propertyInfo.GetValue(Obj, null),
                propertyInfo.GetValue(Obj, null) == null ?
                propertyInfo.PropertyType : propertyInfo.GetValue(Obj, null).GetType(), isPrimeKey, isPrimeKeyRequired);
        }

        private void ParsePropertyInfo(Attributes.DBFieldAttribute dbAttributes, PropertyInfo propertyInfo)
        {
            ParsePropertyInfo(dbAttributes, propertyInfo, false, false);
        }

        /// <summary>
        /// Builds the mapping object and loads the DBCommand object with parameters as required.
        /// </summary>
        /// <param name="columName"></param>
        /// <param name="columnValue"></param>
        /// <param name="nativeType"></param>
        private void BuildColumMapping(string columnName, object columnValue, Type nativeType, bool isPrimeKey, bool isPrimeKeyRequired)
        {
            String parameter = null;
            
            switch (Details.DbEngine)
            {
                case Engine.DBEngine.SqlServer2000:
                case Engine.DBEngine.SqlServer2005:
                    parameter = "@" + columnName;
                    break;

                case Engine.DBEngine.Oracle9i:
                case Engine.DBEngine.Oracle10g:
                    parameter = "?";
                    break;
            }

            if (!isPrimeKey)
            {
                mapping.PersistableMapping.Add(columnName, columnValue);
                mapping.DbObject.DbColumns.Add(new Translation.DBColumn(columnName, columnValue, parameter));
                if(QueryType != QueryType.Select)
                    Command.Parameters.Add(Utility.ParameterBuilder.GetParameter(columnName, parameter, columnValue, nativeType, Details.DbEngine));
            }
            else
            {
                if (isPrimeKeyRequired)
                {
                    mapping.DbObject.DbColumns.Add(new Translation.PrimeKeyColumn(columnName, columnValue, parameter, true));
                    if (QueryType != QueryType.Select)
                    {
                        Command.Parameters.Add(Utility.ParameterBuilder.GetParameter(columnName, parameter, columnValue, 
                            nativeType, Details.DbEngine, isPrimeKeyRequired));
                    }
                    else
                    {
                        Command.Parameters.Add(Utility.ParameterBuilder.GetParameter(columnName, parameter, columnValue, 
                            nativeType, Details.DbEngine));
                    }
                }
                else
                {
                    mapping.DbObject.DbColumns.Add(new Translation.PrimeKeyColumn(columnName, columnValue, parameter, false));
                    // Generated from application so required as standard parameter
                    Command.Parameters.Add(Utility.ParameterBuilder.GetParameter(columnName, parameter, columnValue, 
                        nativeType, Details.DbEngine));
                }
            }
            CheckReserverdWords(columnName);
        }

        private void CheckReserverdWords(string columnName)
        {
            if (Details.ReservedWords.Contains(columnName.ToUpper(System.Globalization.CultureInfo.InvariantCulture)))
                Log.LogWarning(Obj.ToString() + " contains reserved word: \"" + columnName + "\".\r\nThis may cause unexpected behaviour");
        }

        /// <summary>
        /// Parses complex type objects (user defined etc)
        /// Extracts the prime key atrribute
        /// Used to build the insert link between primary/foreign key tables...
        /// </summary>
        /// <param name="dbAttributes"></param>
        /// <param name="o"></param>
        private void ParseComplexObject(Attributes.DBFieldAttribute dbAttributes, object o)
        {
            //PropertyInfo propInfo = null;
            foreach (PropertyInfo propertyInfo in o.GetType().GetProperties())
            {
                // We only want the prime key at this stage...
                if (propertyInfo.GetCustomAttributes(typeof(Attributes.PrimeKeyAttribute), false).Length == 1)
                {
                    BuildColumMapping(dbAttributes.ColumnName == null ?
                    propertyInfo.Name : dbAttributes.ColumnName, propertyInfo.GetValue(o, null),
                    propertyInfo.GetValue(o, null) == null ?
                    propertyInfo.PropertyType : propertyInfo.GetValue(o, null).GetType(), false, false);
                }
            }
        }

        #region IDBSyntax Members
        /// <summary>
        /// Gets the original Object plus the newly created PrimeKey value if applicable
        /// </summary>
        /// <returns></returns>
        public object GetPersistedObject()
        {
            if (PrimeKey != null)
            {
                foreach (PropertyInfo propertyInfo in Obj.GetType().GetProperties())
                {
                    if (propertyInfo.GetCustomAttributes(typeof(Attributes.PrimeKeyAttribute), false).Length == 1)
                    {
                        propertyInfo.SetValue(Obj, PrimeKey, null);
                    }
                }
                return Obj;
            }
            else
            {
                return Obj;
            }
        }
        #endregion
    }
}